import {
  Box3,
  BufferAttribute,
  BufferGeometry,
  Color,
  EventDispatcher,
  Float32BufferAttribute,
  Matrix3,
  Matrix4,
  MathUtils,
  Object3D,
  Sphere,
  Vector2,
  Vector3
} from 'three'

const _m1 = new Matrix4()
const _obj = new Object3D()
const _offset = new Vector3()

class Geometry extends EventDispatcher {
  constructor() {
    super()

    this.uuid = MathUtils.generateUUID()

    this.name = ''
    this.type = 'Geometry'

    this.vertices = []
    this.colors = []
    this.faces = []
    this.faceVertexUvs = [[]]

    this.morphTargets = []
    this.morphNormals = []

    this.skinWeights = []
    this.skinIndices = []

    this.lineDistances = []

    this.boundingBox = null
    this.boundingSphere = null

    // update flags

    this.elementsNeedUpdate = false
    this.verticesNeedUpdate = false
    this.uvsNeedUpdate = false
    this.normalsNeedUpdate = false
    this.colorsNeedUpdate = false
    this.lineDistancesNeedUpdate = false
    this.groupsNeedUpdate = false
  }

  applyMatrix4(matrix) {
    const normalMatrix = new Matrix3().getNormalMatrix(matrix)

    for (let i = 0, il = this.vertices.length; i < il; i++) {
      const vertex = this.vertices[i]
      vertex.applyMatrix4(matrix)
    }

    for (let i = 0, il = this.faces.length; i < il; i++) {
      const face = this.faces[i]
      face.normal.applyMatrix3(normalMatrix).normalize()

      for (let j = 0, jl = face.vertexNormals.length; j < jl; j++) {
        face.vertexNormals[j].applyMatrix3(normalMatrix).normalize()
      }
    }

    if (this.boundingBox !== null) {
      this.computeBoundingBox()
    }

    if (this.boundingSphere !== null) {
      this.computeBoundingSphere()
    }

    this.verticesNeedUpdate = true
    this.normalsNeedUpdate = true

    return this
  }

  rotateX(angle) {
    // rotate geometry around world x-axis

    _m1.makeRotationX(angle)

    this.applyMatrix4(_m1)

    return this
  }

  rotateY(angle) {
    // rotate geometry around world y-axis

    _m1.makeRotationY(angle)

    this.applyMatrix4(_m1)

    return this
  }

  rotateZ(angle) {
    // rotate geometry around world z-axis

    _m1.makeRotationZ(angle)

    this.applyMatrix4(_m1)

    return this
  }

  translate(x, y, z) {
    // translate geometry

    _m1.makeTranslation(x, y, z)

    this.applyMatrix4(_m1)

    return this
  }

  scale(x, y, z) {
    // scale geometry

    _m1.makeScale(x, y, z)

    this.applyMatrix4(_m1)

    return this
  }

  lookAt(vector) {
    _obj.lookAt(vector)

    _obj.updateMatrix()

    this.applyMatrix4(_obj.matrix)

    return this
  }

  fromBufferGeometry(geometry) {
    const scope = this

    const index = geometry.index !== null ? geometry.index : undefined
    const attributes = geometry.attributes

    if (attributes.position === undefined) {
      console.error('THREE.Geometry.fromBufferGeometry(): Position attribute required for conversion.')
      return this
    }

    const position = attributes.position
    const normal = attributes.normal
    const color = attributes.color
    const uv = attributes.uv
    const uv2 = attributes.uv2

    if (uv2 !== undefined) this.faceVertexUvs[1] = []

    for (let i = 0; i < position.count; i++) {
      scope.vertices.push(new Vector3().fromBufferAttribute(position, i))

      if (color !== undefined) {
        scope.colors.push(new Color().fromBufferAttribute(color, i))
      }
    }

    function addFace(a, b, c, materialIndex) {
      const vertexColors =
        color === undefined ? [] : [scope.colors[a].clone(), scope.colors[b].clone(), scope.colors[c].clone()]

      const vertexNormals =
        normal === undefined
          ? []
          : [
              new Vector3().fromBufferAttribute(normal, a),
              new Vector3().fromBufferAttribute(normal, b),
              new Vector3().fromBufferAttribute(normal, c)
            ]

      const face = new Face3(a, b, c, vertexNormals, vertexColors, materialIndex)

      scope.faces.push(face)

      if (uv !== undefined) {
        scope.faceVertexUvs[0].push([
          new Vector2().fromBufferAttribute(uv, a),
          new Vector2().fromBufferAttribute(uv, b),
          new Vector2().fromBufferAttribute(uv, c)
        ])
      }

      if (uv2 !== undefined) {
        scope.faceVertexUvs[1].push([
          new Vector2().fromBufferAttribute(uv2, a),
          new Vector2().fromBufferAttribute(uv2, b),
          new Vector2().fromBufferAttribute(uv2, c)
        ])
      }
    }

    const groups = geometry.groups

    if (groups.length > 0) {
      for (let i = 0; i < groups.length; i++) {
        const group = groups[i]

        const start = group.start
        const count = group.count

        for (let j = start, jl = start + count; j < jl; j += 3) {
          if (index !== undefined) {
            addFace(index.getX(j), index.getX(j + 1), index.getX(j + 2), group.materialIndex)
          } else {
            addFace(j, j + 1, j + 2, group.materialIndex)
          }
        }
      }
    } else {
      if (index !== undefined) {
        for (let i = 0; i < index.count; i += 3) {
          addFace(index.getX(i), index.getX(i + 1), index.getX(i + 2))
        }
      } else {
        for (let i = 0; i < position.count; i += 3) {
          addFace(i, i + 1, i + 2)
        }
      }
    }

    this.computeFaceNormals()

    if (geometry.boundingBox !== null) {
      this.boundingBox = geometry.boundingBox.clone()
    }

    if (geometry.boundingSphere !== null) {
      this.boundingSphere = geometry.boundingSphere.clone()
    }

    return this
  }

  center() {
    this.computeBoundingBox()

    this.boundingBox.getCenter(_offset).negate()

    this.translate(_offset.x, _offset.y, _offset.z)

    return this
  }

  normalize() {
    this.computeBoundingSphere()

    const center = this.boundingSphere.center
    const radius = this.boundingSphere.radius

    const s = radius === 0 ? 1 : 1.0 / radius

    const matrix = new Matrix4()
    matrix.set(s, 0, 0, -s * center.x, 0, s, 0, -s * center.y, 0, 0, s, -s * center.z, 0, 0, 0, 1)

    this.applyMatrix4(matrix)

    return this
  }

  computeFaceNormals() {
    const cb = new Vector3(),
      ab = new Vector3()

    for (let f = 0, fl = this.faces.length; f < fl; f++) {
      const face = this.faces[f]

      const vA = this.vertices[face.a]
      const vB = this.vertices[face.b]
      const vC = this.vertices[face.c]

      cb.subVectors(vC, vB)
      ab.subVectors(vA, vB)
      cb.cross(ab)

      cb.normalize()

      face.normal.copy(cb)
    }
  }

  computeVertexNormals(areaWeighted = true) {
    const vertices = new Array(this.vertices.length)

    for (let v = 0, vl = this.vertices.length; v < vl; v++) {
      vertices[v] = new Vector3()
    }

    if (areaWeighted) {
      // vertex normals weighted by triangle areas
      // http://www.iquilezles.org/www/articles/normals/normals.htm

      const cb = new Vector3(),
        ab = new Vector3()

      for (let f = 0, fl = this.faces.length; f < fl; f++) {
        const face = this.faces[f]

        const vA = this.vertices[face.a]
        const vB = this.vertices[face.b]
        const vC = this.vertices[face.c]

        cb.subVectors(vC, vB)
        ab.subVectors(vA, vB)
        cb.cross(ab)

        vertices[face.a].add(cb)
        vertices[face.b].add(cb)
        vertices[face.c].add(cb)
      }
    } else {
      this.computeFaceNormals()

      for (let f = 0, fl = this.faces.length; f < fl; f++) {
        const face = this.faces[f]

        vertices[face.a].add(face.normal)
        vertices[face.b].add(face.normal)
        vertices[face.c].add(face.normal)
      }
    }

    for (let v = 0, vl = this.vertices.length; v < vl; v++) {
      vertices[v].normalize()
    }

    for (let f = 0, fl = this.faces.length; f < fl; f++) {
      const face = this.faces[f]

      const vertexNormals = face.vertexNormals

      if (vertexNormals.length === 3) {
        vertexNormals[0].copy(vertices[face.a])
        vertexNormals[1].copy(vertices[face.b])
        vertexNormals[2].copy(vertices[face.c])
      } else {
        vertexNormals[0] = vertices[face.a].clone()
        vertexNormals[1] = vertices[face.b].clone()
        vertexNormals[2] = vertices[face.c].clone()
      }
    }

    if (this.faces.length > 0) {
      this.normalsNeedUpdate = true
    }
  }

  computeFlatVertexNormals() {
    this.computeFaceNormals()

    for (let f = 0, fl = this.faces.length; f < fl; f++) {
      const face = this.faces[f]

      const vertexNormals = face.vertexNormals

      if (vertexNormals.length === 3) {
        vertexNormals[0].copy(face.normal)
        vertexNormals[1].copy(face.normal)
        vertexNormals[2].copy(face.normal)
      } else {
        vertexNormals[0] = face.normal.clone()
        vertexNormals[1] = face.normal.clone()
        vertexNormals[2] = face.normal.clone()
      }
    }

    if (this.faces.length > 0) {
      this.normalsNeedUpdate = true
    }
  }

  computeMorphNormals() {
    // save original normals
    // - create temp variables on first access
    //   otherwise just copy (for faster repeated calls)

    for (let f = 0, fl = this.faces.length; f < fl; f++) {
      const face = this.faces[f]

      if (!face.__originalFaceNormal) {
        face.__originalFaceNormal = face.normal.clone()
      } else {
        face.__originalFaceNormal.copy(face.normal)
      }

      if (!face.__originalVertexNormals) face.__originalVertexNormals = []

      for (let i = 0, il = face.vertexNormals.length; i < il; i++) {
        if (!face.__originalVertexNormals[i]) {
          face.__originalVertexNormals[i] = face.vertexNormals[i].clone()
        } else {
          face.__originalVertexNormals[i].copy(face.vertexNormals[i])
        }
      }
    }

    // use temp geometry to compute face and vertex normals for each morph

    const tmpGeo = new Geometry()
    tmpGeo.faces = this.faces

    for (let i = 0, il = this.morphTargets.length; i < il; i++) {
      // create on first access

      if (!this.morphNormals[i]) {
        this.morphNormals[i] = {}
        this.morphNormals[i].faceNormals = []
        this.morphNormals[i].vertexNormals = []

        const dstNormalsFace = this.morphNormals[i].faceNormals
        const dstNormalsVertex = this.morphNormals[i].vertexNormals

        for (let f = 0, fl = this.faces.length; f < fl; f++) {
          const faceNormal = new Vector3()
          const vertexNormals = { a: new Vector3(), b: new Vector3(), c: new Vector3() }

          dstNormalsFace.push(faceNormal)
          dstNormalsVertex.push(vertexNormals)
        }
      }

      const morphNormals = this.morphNormals[i]

      // set vertices to morph target

      tmpGeo.vertices = this.morphTargets[i].vertices

      // compute morph normals

      tmpGeo.computeFaceNormals()
      tmpGeo.computeVertexNormals()

      // store morph normals

      for (let f = 0, fl = this.faces.length; f < fl; f++) {
        const face = this.faces[f]

        const faceNormal = morphNormals.faceNormals[f]
        const vertexNormals = morphNormals.vertexNormals[f]

        faceNormal.copy(face.normal)

        vertexNormals.a.copy(face.vertexNormals[0])
        vertexNormals.b.copy(face.vertexNormals[1])
        vertexNormals.c.copy(face.vertexNormals[2])
      }
    }

    // restore original normals

    for (let f = 0, fl = this.faces.length; f < fl; f++) {
      const face = this.faces[f]

      face.normal = face.__originalFaceNormal
      face.vertexNormals = face.__originalVertexNormals
    }
  }

  computeBoundingBox() {
    if (this.boundingBox === null) {
      this.boundingBox = new Box3()
    }

    this.boundingBox.setFromPoints(this.vertices)
  }

  computeBoundingSphere() {
    if (this.boundingSphere === null) {
      this.boundingSphere = new Sphere()
    }

    this.boundingSphere.setFromPoints(this.vertices)
  }

  merge(geometry, matrix, materialIndexOffset = 0) {
    if (!(geometry && geometry.isGeometry)) {
      console.error('THREE.Geometry.merge(): geometry not an instance of THREE.Geometry.', geometry)
      return
    }

    let normalMatrix
    const vertexOffset = this.vertices.length,
      vertices1 = this.vertices,
      vertices2 = geometry.vertices,
      faces1 = this.faces,
      faces2 = geometry.faces,
      colors1 = this.colors,
      colors2 = geometry.colors

    if (matrix !== undefined) {
      normalMatrix = new Matrix3().getNormalMatrix(matrix)
    }

    // vertices

    for (let i = 0, il = vertices2.length; i < il; i++) {
      const vertex = vertices2[i]

      const vertexCopy = vertex.clone()

      if (matrix !== undefined) vertexCopy.applyMatrix4(matrix)

      vertices1.push(vertexCopy)
    }

    // colors

    for (let i = 0, il = colors2.length; i < il; i++) {
      colors1.push(colors2[i].clone())
    }

    // faces

    for (let i = 0, il = faces2.length; i < il; i++) {
      const face = faces2[i]
      let normal, color
      const faceVertexNormals = face.vertexNormals,
        faceVertexColors = face.vertexColors

      const faceCopy = new Face3(face.a + vertexOffset, face.b + vertexOffset, face.c + vertexOffset)
      faceCopy.normal.copy(face.normal)

      if (normalMatrix !== undefined) {
        faceCopy.normal.applyMatrix3(normalMatrix).normalize()
      }

      for (let j = 0, jl = faceVertexNormals.length; j < jl; j++) {
        normal = faceVertexNormals[j].clone()

        if (normalMatrix !== undefined) {
          normal.applyMatrix3(normalMatrix).normalize()
        }

        faceCopy.vertexNormals.push(normal)
      }

      faceCopy.color.copy(face.color)

      for (let j = 0, jl = faceVertexColors.length; j < jl; j++) {
        color = faceVertexColors[j]
        faceCopy.vertexColors.push(color.clone())
      }

      faceCopy.materialIndex = face.materialIndex + materialIndexOffset

      faces1.push(faceCopy)
    }

    // uvs

    for (let i = 0, il = geometry.faceVertexUvs.length; i < il; i++) {
      const faceVertexUvs2 = geometry.faceVertexUvs[i]

      if (this.faceVertexUvs[i] === undefined) this.faceVertexUvs[i] = []

      for (let j = 0, jl = faceVertexUvs2.length; j < jl; j++) {
        const uvs2 = faceVertexUvs2[j],
          uvsCopy = []

        for (let k = 0, kl = uvs2.length; k < kl; k++) {
          uvsCopy.push(uvs2[k].clone())
        }

        this.faceVertexUvs[i].push(uvsCopy)
      }
    }
  }

  mergeMesh(mesh) {
    if (!(mesh && mesh.isMesh)) {
      console.error('THREE.Geometry.mergeMesh(): mesh not an instance of THREE.Mesh.', mesh)
      return
    }

    if (mesh.matrixAutoUpdate) mesh.updateMatrix()

    this.merge(mesh.geometry, mesh.matrix)
  }

  /*
   * Checks for duplicate vertices with hashmap.
   * Duplicated vertices are removed
   * and faces' vertices are updated.
   */

  mergeVertices(precisionPoints = 4) {
    const verticesMap = {} // Hashmap for looking up vertices by position coordinates (and making sure they are unique)
    const unique = [],
      changes = []

    const precision = Math.pow(10, precisionPoints)

    for (let i = 0, il = this.vertices.length; i < il; i++) {
      const v = this.vertices[i]
      const key = Math.round(v.x * precision) + '_' + Math.round(v.y * precision) + '_' + Math.round(v.z * precision)

      if (verticesMap[key] === undefined) {
        verticesMap[key] = i
        unique.push(this.vertices[i])
        changes[i] = unique.length - 1
      } else {
        //console.log('Duplicate vertex found. ', i, ' could be using ', verticesMap[key]);
        changes[i] = changes[verticesMap[key]]
      }
    }

    // if faces are completely degenerate after merging vertices, we
    // have to remove them from the geometry.
    const faceIndicesToRemove = []

    for (let i = 0, il = this.faces.length; i < il; i++) {
      const face = this.faces[i]

      face.a = changes[face.a]
      face.b = changes[face.b]
      face.c = changes[face.c]

      const indices = [face.a, face.b, face.c]

      // if any duplicate vertices are found in a Face3
      // we have to remove the face as nothing can be saved
      for (let n = 0; n < 3; n++) {
        if (indices[n] === indices[(n + 1) % 3]) {
          faceIndicesToRemove.push(i)
          break
        }
      }
    }

    for (let i = faceIndicesToRemove.length - 1; i >= 0; i--) {
      const idx = faceIndicesToRemove[i]

      this.faces.splice(idx, 1)

      for (let j = 0, jl = this.faceVertexUvs.length; j < jl; j++) {
        this.faceVertexUvs[j].splice(idx, 1)
      }
    }

    // Use unique set of vertices

    const diff = this.vertices.length - unique.length
    this.vertices = unique
    return diff
  }

  setFromPoints(points) {
    this.vertices = []

    for (let i = 0, l = points.length; i < l; i++) {
      const point = points[i]
      this.vertices.push(new Vector3(point.x, point.y, point.z || 0))
    }

    return this
  }

  sortFacesByMaterialIndex() {
    const faces = this.faces
    const length = faces.length

    // tag faces

    for (let i = 0; i < length; i++) {
      faces[i]._id = i
    }

    // sort faces

    function materialIndexSort(a, b) {
      return a.materialIndex - b.materialIndex
    }

    faces.sort(materialIndexSort)

    // sort uvs

    const uvs1 = this.faceVertexUvs[0]
    const uvs2 = this.faceVertexUvs[1]

    let newUvs1, newUvs2

    if (uvs1 && uvs1.length === length) newUvs1 = []
    if (uvs2 && uvs2.length === length) newUvs2 = []

    for (let i = 0; i < length; i++) {
      const id = faces[i]._id

      if (newUvs1) newUvs1.push(uvs1[id])
      if (newUvs2) newUvs2.push(uvs2[id])
    }

    if (newUvs1) this.faceVertexUvs[0] = newUvs1
    if (newUvs2) this.faceVertexUvs[1] = newUvs2
  }

  toJSON() {
    const data = {
      metadata: {
        version: 4.5,
        type: 'Geometry',
        generator: 'Geometry.toJSON'
      }
    }

    // standard Geometry serialization

    data.uuid = this.uuid
    data.type = this.type
    if (this.name !== '') data.name = this.name

    if (this.parameters !== undefined) {
      const parameters = this.parameters

      for (const key in parameters) {
        if (parameters[key] !== undefined) data[key] = parameters[key]
      }

      return data
    }

    const vertices = []

    for (let i = 0; i < this.vertices.length; i++) {
      const vertex = this.vertices[i]
      vertices.push(vertex.x, vertex.y, vertex.z)
    }

    const faces = []
    const normals = []
    const normalsHash = {}
    const colors = []
    const colorsHash = {}
    const uvs = []
    const uvsHash = {}

    for (let i = 0; i < this.faces.length; i++) {
      const face = this.faces[i]

      const hasMaterial = true
      const hasFaceUv = false // deprecated
      const hasFaceVertexUv = this.faceVertexUvs[0][i] !== undefined
      const hasFaceNormal = face.normal.length() > 0
      const hasFaceVertexNormal = face.vertexNormals.length > 0
      const hasFaceColor = face.color.r !== 1 || face.color.g !== 1 || face.color.b !== 1
      const hasFaceVertexColor = face.vertexColors.length > 0

      let faceType = 0

      faceType = setBit(faceType, 0, 0) // isQuad
      faceType = setBit(faceType, 1, hasMaterial)
      faceType = setBit(faceType, 2, hasFaceUv)
      faceType = setBit(faceType, 3, hasFaceVertexUv)
      faceType = setBit(faceType, 4, hasFaceNormal)
      faceType = setBit(faceType, 5, hasFaceVertexNormal)
      faceType = setBit(faceType, 6, hasFaceColor)
      faceType = setBit(faceType, 7, hasFaceVertexColor)

      faces.push(faceType)
      faces.push(face.a, face.b, face.c)
      faces.push(face.materialIndex)

      if (hasFaceVertexUv) {
        const faceVertexUvs = this.faceVertexUvs[0][i]

        faces.push(getUvIndex(faceVertexUvs[0]), getUvIndex(faceVertexUvs[1]), getUvIndex(faceVertexUvs[2]))
      }

      if (hasFaceNormal) {
        faces.push(getNormalIndex(face.normal))
      }

      if (hasFaceVertexNormal) {
        const vertexNormals = face.vertexNormals

        faces.push(getNormalIndex(vertexNormals[0]), getNormalIndex(vertexNormals[1]), getNormalIndex(vertexNormals[2]))
      }

      if (hasFaceColor) {
        faces.push(getColorIndex(face.color))
      }

      if (hasFaceVertexColor) {
        const vertexColors = face.vertexColors

        faces.push(getColorIndex(vertexColors[0]), getColorIndex(vertexColors[1]), getColorIndex(vertexColors[2]))
      }
    }

    function setBit(value, position, enabled) {
      return enabled ? value | (1 << position) : value & ~(1 << position)
    }

    function getNormalIndex(normal) {
      const hash = normal.x.toString() + normal.y.toString() + normal.z.toString()

      if (normalsHash[hash] !== undefined) {
        return normalsHash[hash]
      }

      normalsHash[hash] = normals.length / 3
      normals.push(normal.x, normal.y, normal.z)

      return normalsHash[hash]
    }

    function getColorIndex(color) {
      const hash = color.r.toString() + color.g.toString() + color.b.toString()

      if (colorsHash[hash] !== undefined) {
        return colorsHash[hash]
      }

      colorsHash[hash] = colors.length
      colors.push(color.getHex())

      return colorsHash[hash]
    }

    function getUvIndex(uv) {
      const hash = uv.x.toString() + uv.y.toString()

      if (uvsHash[hash] !== undefined) {
        return uvsHash[hash]
      }

      uvsHash[hash] = uvs.length / 2
      uvs.push(uv.x, uv.y)

      return uvsHash[hash]
    }

    data.data = {}

    data.data.vertices = vertices
    data.data.normals = normals
    if (colors.length > 0) data.data.colors = colors
    if (uvs.length > 0) data.data.uvs = [uvs] // temporal backward compatibility
    data.data.faces = faces

    return data
  }

  clone() {
    /*
		 // Handle primitives

		 const parameters = this.parameters;

		 if ( parameters !== undefined ) {

		 const values = [];

		 for ( const key in parameters ) {

		 values.push( parameters[ key ] );

		 }

		 const geometry = Object.create( this.constructor.prototype );
		 this.constructor.apply( geometry, values );
		 return geometry;

		 }

		 return new this.constructor().copy( this );
		 */

    return new Geometry().copy(this)
  }

  copy(source) {
    // reset

    this.vertices = []
    this.colors = []
    this.faces = []
    this.faceVertexUvs = [[]]
    this.morphTargets = []
    this.morphNormals = []
    this.skinWeights = []
    this.skinIndices = []
    this.lineDistances = []
    this.boundingBox = null
    this.boundingSphere = null

    // name

    this.name = source.name

    // vertices

    const vertices = source.vertices

    for (let i = 0, il = vertices.length; i < il; i++) {
      this.vertices.push(vertices[i].clone())
    }

    // colors

    const colors = source.colors

    for (let i = 0, il = colors.length; i < il; i++) {
      this.colors.push(colors[i].clone())
    }

    // faces

    const faces = source.faces

    for (let i = 0, il = faces.length; i < il; i++) {
      this.faces.push(faces[i].clone())
    }

    // face vertex uvs

    for (let i = 0, il = source.faceVertexUvs.length; i < il; i++) {
      const faceVertexUvs = source.faceVertexUvs[i]

      if (this.faceVertexUvs[i] === undefined) {
        this.faceVertexUvs[i] = []
      }

      for (let j = 0, jl = faceVertexUvs.length; j < jl; j++) {
        const uvs = faceVertexUvs[j],
          uvsCopy = []

        for (let k = 0, kl = uvs.length; k < kl; k++) {
          const uv = uvs[k]

          uvsCopy.push(uv.clone())
        }

        this.faceVertexUvs[i].push(uvsCopy)
      }
    }

    // morph targets

    const morphTargets = source.morphTargets

    for (let i = 0, il = morphTargets.length; i < il; i++) {
      const morphTarget = {}
      morphTarget.name = morphTargets[i].name

      // vertices

      if (morphTargets[i].vertices !== undefined) {
        morphTarget.vertices = []

        for (let j = 0, jl = morphTargets[i].vertices.length; j < jl; j++) {
          morphTarget.vertices.push(morphTargets[i].vertices[j].clone())
        }
      }

      // normals

      if (morphTargets[i].normals !== undefined) {
        morphTarget.normals = []

        for (let j = 0, jl = morphTargets[i].normals.length; j < jl; j++) {
          morphTarget.normals.push(morphTargets[i].normals[j].clone())
        }
      }

      this.morphTargets.push(morphTarget)
    }

    // morph normals

    const morphNormals = source.morphNormals

    for (let i = 0, il = morphNormals.length; i < il; i++) {
      const morphNormal = {}

      // vertex normals

      if (morphNormals[i].vertexNormals !== undefined) {
        morphNormal.vertexNormals = []

        for (let j = 0, jl = morphNormals[i].vertexNormals.length; j < jl; j++) {
          const srcVertexNormal = morphNormals[i].vertexNormals[j]
          const destVertexNormal = {}

          destVertexNormal.a = srcVertexNormal.a.clone()
          destVertexNormal.b = srcVertexNormal.b.clone()
          destVertexNormal.c = srcVertexNormal.c.clone()

          morphNormal.vertexNormals.push(destVertexNormal)
        }
      }

      // face normals

      if (morphNormals[i].faceNormals !== undefined) {
        morphNormal.faceNormals = []

        for (let j = 0, jl = morphNormals[i].faceNormals.length; j < jl; j++) {
          morphNormal.faceNormals.push(morphNormals[i].faceNormals[j].clone())
        }
      }

      this.morphNormals.push(morphNormal)
    }

    // skin weights

    const skinWeights = source.skinWeights

    for (let i = 0, il = skinWeights.length; i < il; i++) {
      this.skinWeights.push(skinWeights[i].clone())
    }

    // skin indices

    const skinIndices = source.skinIndices

    for (let i = 0, il = skinIndices.length; i < il; i++) {
      this.skinIndices.push(skinIndices[i].clone())
    }

    // line distances

    const lineDistances = source.lineDistances

    for (let i = 0, il = lineDistances.length; i < il; i++) {
      this.lineDistances.push(lineDistances[i])
    }

    // bounding box

    const boundingBox = source.boundingBox

    if (boundingBox !== null) {
      this.boundingBox = boundingBox.clone()
    }

    // bounding sphere

    const boundingSphere = source.boundingSphere

    if (boundingSphere !== null) {
      this.boundingSphere = boundingSphere.clone()
    }

    // update flags

    this.elementsNeedUpdate = source.elementsNeedUpdate
    this.verticesNeedUpdate = source.verticesNeedUpdate
    this.uvsNeedUpdate = source.uvsNeedUpdate
    this.normalsNeedUpdate = source.normalsNeedUpdate
    this.colorsNeedUpdate = source.colorsNeedUpdate
    this.lineDistancesNeedUpdate = source.lineDistancesNeedUpdate
    this.groupsNeedUpdate = source.groupsNeedUpdate

    return this
  }

  toBufferGeometry() {
    const geometry = new DirectGeometry().fromGeometry(this)

    const buffergeometry = new BufferGeometry()

    const positions = new Float32Array(geometry.vertices.length * 3)
    buffergeometry.setAttribute('position', new BufferAttribute(positions, 3).copyVector3sArray(geometry.vertices))

    if (geometry.normals.length > 0) {
      const normals = new Float32Array(geometry.normals.length * 3)
      buffergeometry.setAttribute('normal', new BufferAttribute(normals, 3).copyVector3sArray(geometry.normals))
    }

    if (geometry.colors.length > 0) {
      const colors = new Float32Array(geometry.colors.length * 3)
      buffergeometry.setAttribute('color', new BufferAttribute(colors, 3).copyColorsArray(geometry.colors))
    }

    if (geometry.uvs.length > 0) {
      const uvs = new Float32Array(geometry.uvs.length * 2)
      buffergeometry.setAttribute('uv', new BufferAttribute(uvs, 2).copyVector2sArray(geometry.uvs))
    }

    if (geometry.uvs2.length > 0) {
      const uvs2 = new Float32Array(geometry.uvs2.length * 2)
      buffergeometry.setAttribute('uv2', new BufferAttribute(uvs2, 2).copyVector2sArray(geometry.uvs2))
    }

    // groups

    buffergeometry.groups = geometry.groups

    // morphs

    for (const name in geometry.morphTargets) {
      const array = []
      const morphTargets = geometry.morphTargets[name]

      for (let i = 0, l = morphTargets.length; i < l; i++) {
        const morphTarget = morphTargets[i]

        const attribute = new Float32BufferAttribute(morphTarget.data.length * 3, 3)
        attribute.name = morphTarget.name

        array.push(attribute.copyVector3sArray(morphTarget.data))
      }

      buffergeometry.morphAttributes[name] = array
    }

    // skinning

    if (geometry.skinIndices.length > 0) {
      const skinIndices = new Float32BufferAttribute(geometry.skinIndices.length * 4, 4)
      buffergeometry.setAttribute('skinIndex', skinIndices.copyVector4sArray(geometry.skinIndices))
    }

    if (geometry.skinWeights.length > 0) {
      const skinWeights = new Float32BufferAttribute(geometry.skinWeights.length * 4, 4)
      buffergeometry.setAttribute('skinWeight', skinWeights.copyVector4sArray(geometry.skinWeights))
    }

    //

    if (geometry.boundingSphere !== null) {
      buffergeometry.boundingSphere = geometry.boundingSphere.clone()
    }

    if (geometry.boundingBox !== null) {
      buffergeometry.boundingBox = geometry.boundingBox.clone()
    }

    return buffergeometry
  }

  computeTangents() {
    console.error('THREE.Geometry: .computeTangents() has been removed.')
  }

  computeLineDistances() {
    console.error(
      'THREE.Geometry: .computeLineDistances() has been removed. Use THREE.Line.computeLineDistances() instead.'
    )
  }

  applyMatrix(matrix) {
    console.warn('THREE.Geometry: .applyMatrix() has been renamed to .applyMatrix4().')
    return this.applyMatrix4(matrix)
  }

  dispose() {
    this.dispatchEvent({ type: 'dispose' })
  }

  static createBufferGeometryFromObject(object) {
    let buffergeometry = new BufferGeometry()

    const geometry = object.geometry

    if (object.isPoints || object.isLine) {
      const positions = new Float32BufferAttribute(geometry.vertices.length * 3, 3)
      const colors = new Float32BufferAttribute(geometry.colors.length * 3, 3)

      buffergeometry.setAttribute('position', positions.copyVector3sArray(geometry.vertices))
      buffergeometry.setAttribute('color', colors.copyColorsArray(geometry.colors))

      if (geometry.lineDistances && geometry.lineDistances.length === geometry.vertices.length) {
        const lineDistances = new Float32BufferAttribute(geometry.lineDistances.length, 1)

        buffergeometry.setAttribute('lineDistance', lineDistances.copyArray(geometry.lineDistances))
      }

      if (geometry.boundingSphere !== null) {
        buffergeometry.boundingSphere = geometry.boundingSphere.clone()
      }

      if (geometry.boundingBox !== null) {
        buffergeometry.boundingBox = geometry.boundingBox.clone()
      }
    } else if (object.isMesh) {
      buffergeometry = geometry.toBufferGeometry()
    }

    return buffergeometry
  }
}

Geometry.prototype.isGeometry = true

class DirectGeometry {
  constructor() {
    this.vertices = []
    this.normals = []
    this.colors = []
    this.uvs = []
    this.uvs2 = []

    this.groups = []

    this.morphTargets = {}

    this.skinWeights = []
    this.skinIndices = []

    // this.lineDistances = [];

    this.boundingBox = null
    this.boundingSphere = null

    // update flags

    this.verticesNeedUpdate = false
    this.normalsNeedUpdate = false
    this.colorsNeedUpdate = false
    this.uvsNeedUpdate = false
    this.groupsNeedUpdate = false
  }

  computeGroups(geometry) {
    const groups = []

    let group, i
    let materialIndex = undefined

    const faces = geometry.faces

    for (i = 0; i < faces.length; i++) {
      const face = faces[i]

      // materials

      if (face.materialIndex !== materialIndex) {
        materialIndex = face.materialIndex

        if (group !== undefined) {
          group.count = i * 3 - group.start
          groups.push(group)
        }

        group = {
          start: i * 3,
          materialIndex: materialIndex
        }
      }
    }

    if (group !== undefined) {
      group.count = i * 3 - group.start
      groups.push(group)
    }

    this.groups = groups
  }

  fromGeometry(geometry) {
    const faces = geometry.faces
    const vertices = geometry.vertices
    const faceVertexUvs = geometry.faceVertexUvs

    const hasFaceVertexUv = faceVertexUvs[0] && faceVertexUvs[0].length > 0
    const hasFaceVertexUv2 = faceVertexUvs[1] && faceVertexUvs[1].length > 0

    // morphs

    const morphTargets = geometry.morphTargets
    const morphTargetsLength = morphTargets.length

    let morphTargetsPosition

    if (morphTargetsLength > 0) {
      morphTargetsPosition = []

      for (let i = 0; i < morphTargetsLength; i++) {
        morphTargetsPosition[i] = {
          name: morphTargets[i].name,
          data: []
        }
      }

      this.morphTargets.position = morphTargetsPosition
    }

    const morphNormals = geometry.morphNormals
    const morphNormalsLength = morphNormals.length

    let morphTargetsNormal

    if (morphNormalsLength > 0) {
      morphTargetsNormal = []

      for (let i = 0; i < morphNormalsLength; i++) {
        morphTargetsNormal[i] = {
          name: morphNormals[i].name,
          data: []
        }
      }

      this.morphTargets.normal = morphTargetsNormal
    }

    // skins

    const skinIndices = geometry.skinIndices
    const skinWeights = geometry.skinWeights

    const hasSkinIndices = skinIndices.length === vertices.length
    const hasSkinWeights = skinWeights.length === vertices.length

    //

    if (vertices.length > 0 && faces.length === 0) {
      console.error('THREE.DirectGeometry: Faceless geometries are not supported.')
    }

    for (let i = 0; i < faces.length; i++) {
      const face = faces[i]

      this.vertices.push(vertices[face.a], vertices[face.b], vertices[face.c])

      const vertexNormals = face.vertexNormals

      if (vertexNormals.length === 3) {
        this.normals.push(vertexNormals[0], vertexNormals[1], vertexNormals[2])
      } else {
        const normal = face.normal

        this.normals.push(normal, normal, normal)
      }

      const vertexColors = face.vertexColors

      if (vertexColors.length === 3) {
        this.colors.push(vertexColors[0], vertexColors[1], vertexColors[2])
      } else {
        const color = face.color

        this.colors.push(color, color, color)
      }

      if (hasFaceVertexUv === true) {
        const vertexUvs = faceVertexUvs[0][i]

        if (vertexUvs !== undefined) {
          this.uvs.push(vertexUvs[0], vertexUvs[1], vertexUvs[2])
        } else {
          console.warn('THREE.DirectGeometry.fromGeometry(): Undefined vertexUv ', i)

          this.uvs.push(new Vector2(), new Vector2(), new Vector2())
        }
      }

      if (hasFaceVertexUv2 === true) {
        const vertexUvs = faceVertexUvs[1][i]

        if (vertexUvs !== undefined) {
          this.uvs2.push(vertexUvs[0], vertexUvs[1], vertexUvs[2])
        } else {
          console.warn('THREE.DirectGeometry.fromGeometry(): Undefined vertexUv2 ', i)

          this.uvs2.push(new Vector2(), new Vector2(), new Vector2())
        }
      }

      // morphs

      for (let j = 0; j < morphTargetsLength; j++) {
        const morphTarget = morphTargets[j].vertices

        morphTargetsPosition[j].data.push(morphTarget[face.a], morphTarget[face.b], morphTarget[face.c])
      }

      for (let j = 0; j < morphNormalsLength; j++) {
        const morphNormal = morphNormals[j].vertexNormals[i]

        morphTargetsNormal[j].data.push(morphNormal.a, morphNormal.b, morphNormal.c)
      }

      // skins

      if (hasSkinIndices) {
        this.skinIndices.push(skinIndices[face.a], skinIndices[face.b], skinIndices[face.c])
      }

      if (hasSkinWeights) {
        this.skinWeights.push(skinWeights[face.a], skinWeights[face.b], skinWeights[face.c])
      }
    }

    this.computeGroups(geometry)

    this.verticesNeedUpdate = geometry.verticesNeedUpdate
    this.normalsNeedUpdate = geometry.normalsNeedUpdate
    this.colorsNeedUpdate = geometry.colorsNeedUpdate
    this.uvsNeedUpdate = geometry.uvsNeedUpdate
    this.groupsNeedUpdate = geometry.groupsNeedUpdate

    if (geometry.boundingSphere !== null) {
      this.boundingSphere = geometry.boundingSphere.clone()
    }

    if (geometry.boundingBox !== null) {
      this.boundingBox = geometry.boundingBox.clone()
    }

    return this
  }
}

class Face3 {
  constructor(a, b, c, normal, color, materialIndex = 0) {
    this.a = a
    this.b = b
    this.c = c

    this.normal = normal && normal.isVector3 ? normal : new Vector3()
    this.vertexNormals = Array.isArray(normal) ? normal : []

    this.color = color && color.isColor ? color : new Color()
    this.vertexColors = Array.isArray(color) ? color : []

    this.materialIndex = materialIndex
  }

  clone() {
    return new this.constructor().copy(this)
  }

  copy(source) {
    this.a = source.a
    this.b = source.b
    this.c = source.c

    this.normal.copy(source.normal)
    this.color.copy(source.color)

    this.materialIndex = source.materialIndex

    for (let i = 0, il = source.vertexNormals.length; i < il; i++) {
      this.vertexNormals[i] = source.vertexNormals[i].clone()
    }

    for (let i = 0, il = source.vertexColors.length; i < il; i++) {
      this.vertexColors[i] = source.vertexColors[i].clone()
    }

    return this
  }
}

export { Face3, Geometry }
